/*
* The MIT License
*
* Copyright (c) 2011-2013, CloudBees, Inc., Stephen Connolly.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.branch;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.BulkChange;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.XmlFile;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.JobProperty;
import hudson.model.Project;
import hudson.model.Run;
import hudson.model.Saveable;
import hudson.model.TopLevelItem;
import hudson.tasks.BuildWrapper;
import hudson.tasks.Publisher;
import hudson.util.DescribableList;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMHead.HeadByItem;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMSource;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
/**
* Creates instances of the branch projects for a specific {@link Branch} and also provides some utility methods for
* updating the branch specific projects.
*
* Please define a 'getting-started' view for a subclass, if you would like to provide specific information to the user
* how to get started using the type of project factory. This view is displayed when there are no subfolders found.
*
* @param <P> the type of the branch projects.
* @param <R> the type of the builds of the branch projects.
* @author Stephen Connolly
*/
public abstract class BranchProjectFactory<P extends Job<P, R> & TopLevelItem,
R extends Run<P, R>>
extends AbstractDescribableImpl<BranchProjectFactory<?, ?>> implements Saveable, ExtensionPoint {
/**
* The owning {@link MultiBranchProject}.
*/
@CheckForNull
private MultiBranchProject<P, R> owner = null;
/**
* Creates a new branch project.
* {@link Item#getName} must match {@link Branch#getEncodedName}.
* @param branch the branch.
* @return the new branch project instance.
*/
public abstract P newInstance(Branch branch);
/**
* Saves the {@link BranchProjectFactory}
*
* @throws IOException if issues saving.
*/
@Override
public void save() throws IOException {
if (owner != null) {
owner.save();
}
}
/**
* Sets the owner.
*
* @param owner the owner.
*/
public void setOwner(MultiBranchProject<P, R> owner) {
this.owner = owner;
}
/**
* Gets the current owner.
*
* @return the current owner.
*/
public MultiBranchProject<P, R> getOwner() {
return owner;
}
/**
* Gets the {@link Branch} that a specific project was configured for.
*
* @param project the project; should assume {@link #isProject} has already been tested on it
* @return the {@link Branch} that the project was configured for.
*/
@NonNull
public abstract Branch getBranch(@NonNull P project);
/**
* Replace the {@link Branch} that a project was configured for with a new updated {@link Branch}.
*
* @param project the project.
* @param branch the new branch.
* @return the project.
*/
@NonNull
public abstract P setBranch(@NonNull P project, @NonNull Branch branch);
/**
* Test if the specified {@link Item} is the branch project type supported by this {@link BranchProjectFactory}
*
* @param item the {@link Item}
* @return {@code true} if and only if the {@link Item} is a project supported by this {@link BranchProjectFactory}.
*/
public abstract boolean isProject(@CheckForNull Item item);
/**
* Casts the {@link Item} into the project type supported by this {@link BranchProjectFactory}.
*
* @param item the {@link Item}.
* @return the {@link Item} upcast to the project type supported by this {@link BranchProjectFactory}.
*/
@NonNull
@SuppressWarnings("unchecked") // type bashing
public P asProject(@NonNull Item item) {
return (P) item;
}
/**
* Gets the {@link SCMRevision} that the project was last built for.
*
* @param project the project.
* @return the {@link SCMRevision} of the last build.
*/
@CheckForNull
public SCMRevision getRevision(P project) {
XmlFile file = new XmlFile(new File(project.getRootDir(), "scm-revision-hash.xml"));
try {
return (SCMRevision) file.read();
} catch (IOException e) {
// ignore
}
return null;
}
/**
* Sets the {@link SCMRevision} that the project was last built for.
*
* @param project the project.
* @param revision the {@link SCMRevision} of the last build.
* @throws IOException if there was an issue persisting the details.
*/
public void setRevisionHash(P project, SCMRevision revision) throws IOException {
XmlFile file = new XmlFile(new File(project.getRootDir(), "scm-revision-hash.xml"));
file.write(revision);
}
/**
* Decorates the project in with all the {@link JobDecorator} instances.
* NOTE: This method should suppress saving the project and only affect the in-memory state.
* NOTE: Override if the default strategy is not appropriate for the specific project type.
*
* @param project the project.
* @return the project for nicer method chaining
*/
@SuppressWarnings({"ConstantConditions", "unchecked"})
public P decorate(P project) {
if (!isProject(project)) {
return project;
}
Branch branch = getBranch(project);
// HACK ALERT
// ==========
// We don't want to trigger a save, so we will do some trickery to inject the new values
// it would be better if Core gave us some hooks to do this
BulkChange bc = new BulkChange(project);
try {
List<BranchProperty> properties = new ArrayList<BranchProperty>(branch.getProperties());
Collections.sort(properties, DescriptorOrder.reverse(BranchProperty.class));
for (BranchProperty property : properties) {
JobDecorator<P, R> decorator = property.jobDecorator(project.getClass());
if (decorator != null) {
// if Project then we can feed the publishers and build wrappers
if (project instanceof Project && decorator instanceof ProjectDecorator) {
DescribableList<Publisher, Descriptor<Publisher>> publishersList = ((Project) project).getPublishersList();
DescribableList<BuildWrapper, Descriptor<BuildWrapper>> buildWrappersList = ((Project) project).getBuildWrappersList();
List<Publisher> publishers = ((ProjectDecorator) decorator).publishers(publishersList.toList());
List<BuildWrapper> buildWrappers = ((ProjectDecorator) decorator).buildWrappers(buildWrappersList.toList());
publishersList.replaceBy(publishers);
buildWrappersList.replaceBy(buildWrappers);
}
// we can always feed the job properties... but just not as easily as we'd like
List<JobProperty<? super P>> jobProperties = decorator.jobProperties(project.getAllProperties());
// HACK: need to replace all properties but no nice method... we will iterate our way through
// both removal and addition
for (JobProperty<? super P> p : project.getAllProperties()) {
project.removeProperty(p);
}
for (JobProperty<? super P> p : jobProperties) {
project.addProperty(p);
}
// now apply the final layer
decorator.project(project);
}
}
} catch (IOException e) {
// should be safe to ignore as the BulkChange suppresses the save operation.
} finally {
bc.abort();
}
return project;
}
/**
* {@inheritDoc}
*/
@Override
@NonNull
public BranchProjectFactoryDescriptor getDescriptor() {
return (BranchProjectFactoryDescriptor) super.getDescriptor();
}
/**
* Returns the base class of the projects that are produced by this factory.
*
* @return the base class of the projects that are produced by this factory.
* @since 2.0
*/
@SuppressWarnings("unchecked")
@NonNull
public final Class<P> getProjectClass() {
return (Class<P>) getDescriptor().getProjectClass();
}
@Restricted(DoNotUse.class)
@Extension
public static class HeadByItemImpl extends HeadByItem {
/** {@inheritDoc} */
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public SCMHead getHead(Item item) {
if (item instanceof Job) {
ItemGroup<?> parent = item.getParent();
if (parent instanceof MultiBranchProject) {
BranchProjectFactory projectFactory = ((MultiBranchProject) parent).getProjectFactory();
if (projectFactory.isProject(item)) {
return projectFactory.getBranch(projectFactory.asProject(item)).getHead();
}
}
}
return null;
}
}
@Restricted(DoNotUse.class)
@Extension
public static class SourceByItemImpl extends SCMSource.SourceByItem {
/** {@inheritDoc} */
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public SCMSource getSource(Item item) {
if (item instanceof Job) {
ItemGroup<?> parent = item.getParent();
if (parent instanceof MultiBranchProject) {
BranchProjectFactory projectFactory = ((MultiBranchProject) parent).getProjectFactory();
if (projectFactory.isProject(item)) {
String sourceId = projectFactory.getBranch(projectFactory.asProject(item)).getSourceId();
return ((MultiBranchProject) parent).getSCMSource(sourceId);
}
}
}
return null;
}
}
}